...loading
2024-12-24
프론트엔드의 꽃과 같은 성능 개선을 시도해봅니다. 이번 프로젝트에서 nextJs를 채택한 이유는 모노레포로 풀스택을 개발하는 것도 있지만, NextJS의 SSR과 서버컴포넌트의 장점을 통해 성능의 이점을 가져가는 목적도 있습니다. 현재 기본적인 CRUD도 개발되었고 프로젝트의 구조도 어느정도 잡혀있기에 성능 개선을 시작합니다.
성능 개선의 첫 타깃은 메인 페이지의 캐러셀입니다. 현재 캐러셀 이미지의 로딩 지연과 함께 깜빡임(플리커링)이 발생합니다.
// 최신 Dev posts 캐러셀 "use client"; ... export default function DevPosts() { const [cardData, setCardData] = useState<PostDataProps[]>([]); useEffect(() => { const fetchData = async () => { try { const response = await fetch("/api/post/get-all-posts"); if (!response.ok) { throw new Error("Failed to fetch data"); } const result = await response.json(); setCardData(sortRecentDevPosts(result)); } catch (error) { console.error("Error fetching data:", error); } }; fetchData(); }, []); return ( <div className={styles["wrapper"]}> <header className={styles["header"]}> <h1 className={styles["title"]}>Dev Posts</h1> <NavButton text="More Posts" size="small" link="/posts/development" /> </header> <main className={styles["content-wrapper"]}> <Carousel data={cardData} /> </main> </div> ); }
기존의 코드는 클라이언트 컴포넌트로 작성되었습니다. useEffect를 통해 data fetching을 진행하고 있습니다. NextJs의 특성을 잘 이해하지 못하고, 기능을 중점으로 구현하다보니 서버컴포넌트의 장점을 잘 살리지 못했습니다.
리액트의 방식이면, 자연스러울 수 있는 코드입니다. 하지만 Nextjs를 사용하고 있는 상황을 고려하면 위 컴포넌트는 Hydration이 완료된 후, useEffect가 실행되고 나서야 데이터 패칭이 이루어집니다. 이러한 상황에서 서버 컴포넌트에서 데이터를 패칭하는 방식을 적용하면, 더 빠른 렌더링을 기대할 수 있습니다.
따라서 위 컴포넌트를 서버컴포넌트로 전환한 후 데이터 패칭을 바로 진행했고, 결과적으로 사용자가 더 빠르게 컨텐츠를 확인할 수 있게 되었습니다.
... export default async function DevPosts() { const fetchResult = await fetchPostList(); const data = sortRecentDevPosts(fetchResult); return ( <Suspense fallback={<p>...loading</p>}> <div className={styles["wrapper"]}> <header className={styles["header"]}> <h1 className={styles["title"]}>Dev posts</h1> <NavButton text="More Posts" size="small" link="/posts/development" /> </header> <main className={styles["content-wrapper"]}> <Carousel data={data} /> </main> </div> </Suspense> ); }
개선된 코드입니다. 서버 컴포넌트의 데이터패칭은 간단합니다. 기존의 함수 컴포넌트에 async를 적용하면 서버 측에서 데이터를 미리 불러올 수 있습니다.
미리 작성한 포스팅 리스트를 가져오는 fetch함수를 통해 데이터를 서버측에서 바로 받아 작업을 처리합니다. 이처럼 서버측에서 바로 데이터가 포함된 HTML을 생성하여 클라이언트에 전달하여 사용자에게 컨텐츠를 더 빠르게 노출할 수 있습니다.
서버 컴포넌트의 성능 차이를 확인해보겠습니다. 오른쪽 Dev posts가 서버 컴포넌트를 적용한 캐러셀입니다. 육안 상으로도 깜빡임이 거의 느껴지지 않습니다.
앞선 작업 이후 Lighthouse를 통해 성능 테스트를 해봤습니다. 생각만큼 양호한 점수가 나오지는 않더군요.. 90점 이상은 나올거라 생각했는데 80점 밖에 나오지 않습니다.
Lighthouse에서 분석한 문제는 다음과 같습니다. 역시나 메인 페이지의 고화질 이미지가 문제인 것 같네요.. 배경 이미지 때문에 FCP와 LGP 측정치 모두 좋지 못하게 나오고 있습니다. 그리고 두 번째 개선점은 폰트입니다. 폰트가 클라이언트에 완전히 적용되기 이전까지 약간의 딜레이가 발생합니다. 따라서 이 부분도 같이 개선을 시도합니다.
import Image from "next/image"; ... // 기존 코드 <Image src={wallpaper} alt="wallpaper" fill/> // 개선된 코드 <Image src={wallpaper} alt="wallpaper" quality={60} fill priority />
이미지를 개선하는 기본적인 방법은 간단합니다. nextJs의 Image태그에서 지원하는 속성을 사용하면 최적화에 큰 도움이 됩니다. 우선 priority 속성을 추가하여 해당 배경이미지가 가장 먼저 뷰포트에 로드될 수 있도록 설정을 해줍니다. 그리고 새로 알게된 Image태그의 quality라는 속성을 적용해봅니다. 디폴트값으로 75%가 설정되어 있는데 60%로 낮추어 기존 이미지 크기의 부담을 줄여줍니다. 실제로 성능 측정 결과에서 이미지 크기를 소폭 줄일 수 있었습니다. (60% 이하는 애정하는 프로젝트의 퀄리티를 위해 양보할 수 없습니다..)
이정도까지 해야할까 싶지만 폰트의 로딩도 최적화를 시켜줍니다. 현재 지정한 폰트 디자인이 적용되어 클라이언트에 표시되기까지 약간의 딜레이가 발생합니다. 이 경우에 font-display : swap
을 적용하면 폰트가 로드되기 전까지 시스템 폰트를 적용하여 글자를 보여줄 수 있습니다. 적용 코드는 간단합니다. 아래와 같습니다.
@font-face { font-family: 'CustomFont'; src: url('/fonts/custom-font.woff2') format('woff2'); font-display: swap; }
Lighthouse가 분석해준 문제들을 개선했습니다. 이제 어떠한 점수가 나오는지 다시 테스트해봅니다.
결과는 97점, 이전에 비해 약 21% 정도의 성능 개선을 달성했습니다! 100점이 아닌건 좀 아쉽지만, 아무래도 100점을 달성하기 위해서는 배경화면의 이미지 퀄리티를 상당하게 줄이거나 디자인을 바꿔야하지 않을까 싶습니다. 하지만 기존의 디자인을 유지한 상태로 97점이라는 좋은 성능을 달성할 수 있었기에 만족스러운 결과입니다.
Comments